package edu.northwestern.cbits.purple_robot_manager.probes.builtin; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.pm.PackageManager; import android.location.Location; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import android.provider.Settings; import android.support.v4.content.ContextCompat; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.location.LocationListener; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationServices; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.HashMap; import java.util.Map; import edu.northwestern.cbits.purple_robot_manager.R; import edu.northwestern.cbits.purple_robot_manager.activities.probes.LocationLabelActivity; import edu.northwestern.cbits.purple_robot_manager.activities.probes.LocationProbeActivity; import edu.northwestern.cbits.purple_robot_manager.activities.settings.FlexibleListPreference; import edu.northwestern.cbits.purple_robot_manager.calibration.LocationCalibrationHelper; import edu.northwestern.cbits.purple_robot_manager.db.ProbeValuesProvider; import edu.northwestern.cbits.purple_robot_manager.logging.LogManager; import edu.northwestern.cbits.purple_robot_manager.logging.SanityCheck; import edu.northwestern.cbits.purple_robot_manager.logging.SanityManager; import edu.northwestern.cbits.purple_robot_manager.probes.Probe; import edu.northwestern.cbits.purple_robot_manager.probes.services.FoursquareProbe; public class FusedLocationProbe extends Probe implements GoogleApiClient.ConnectionCallbacks, LocationListener, GoogleApiClient.OnConnectionFailedListener { public static final String NAME = "edu.northwestern.cbits.purple_robot_manager.probes.builtin.FusedLocationProbe"; public static final String LATITUDE = "LATITUDE"; public static final String LONGITUDE = "LONGITUDE"; private static final String PROVIDER = "PROVIDER"; private static final String ACCURACY = "ACCURACY"; private static final String ALTITUDE = "ALTITUDE"; private static final String BEARING = "BEARING"; private static final String SPEED = "SPEED"; private static final String TIME_FIX = "TIME_FIX"; public static final String LATITUDE_KEY = LATITUDE; public static final String LONGITUDE_KEY = LONGITUDE; private static final long LOCATION_TIMEOUT = (1000 * 60 * 5); public static final String DB_TABLE = "fused_location_probe"; public static String ENABLED = "config_probe_fused_location_enabled"; public static String FREQUENCY = "config_probe_fused_location_frequency"; public static final String DISTANCE = "config_probe_fused_location_distance"; public static final boolean DEFAULT_ENABLED = false; public static final String DEFAULT_DISTANCE = "800"; public static final boolean DEFAULT_ENABLE_CALIBRATION_NOTIFICATIONS = true; public static final String ENABLE_CALIBRATION_NOTIFICATIONS = "config_probe_fused_location_calibration_notifications"; protected Context _context = null; private long _lastFrequency = 0; private boolean _listening = false; private final HashMap<String, Boolean> _lastEnabled = new HashMap<>(); private final HashMap<String, Integer> _lastStatus = new HashMap<>(); private GoogleApiClient _apiClient = null; private long _lastDistance = 0; private long _lastCache = 0; private Location _lastLocation = null; private long _probeEnabled = 0; private long _lastReading = 0; @Override public String getPreferenceKey() { return "built_in_fused_location"; } @Override public String probeCategory(Context context) { return context.getString(R.string.probe_sensor_category); } @Override public void enable(Context context) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean(FusedLocationProbe.ENABLED, true); e.commit(); } @Override public void disable(Context context) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean(FusedLocationProbe.ENABLED, false); e.commit(); } @Override public Map<String, Object> configuration(Context context) { Map<String, Object> map = super.configuration(context); SharedPreferences prefs = Probe.getPreferences(context); long freq = Long.parseLong(prefs.getString(FusedLocationProbe.FREQUENCY, Probe.DEFAULT_FREQUENCY)); map.put(Probe.PROBE_FREQUENCY, freq); long distance = Long.parseLong(prefs.getString(FusedLocationProbe.DISTANCE, FusedLocationProbe.DEFAULT_DISTANCE)); map.put(Probe.PROBE_DISTANCE, distance); boolean calibrateNotes = prefs.getBoolean(FusedLocationProbe.ENABLE_CALIBRATION_NOTIFICATIONS, FusedLocationProbe.DEFAULT_ENABLE_CALIBRATION_NOTIFICATIONS); map.put(Probe.PROBE_CALIBRATION_NOTIFICATIONS, calibrateNotes); return map; } @Override public void updateFromMap(Context context, Map<String, Object> params) { super.updateFromMap(context, params); if (params.containsKey(Probe.PROBE_FREQUENCY)) { Object frequency = params.get(Probe.PROBE_FREQUENCY); if (frequency instanceof Double) { frequency = ((Double) frequency).longValue(); } if (frequency instanceof Long) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putString(FusedLocationProbe.FREQUENCY, frequency.toString()); e.commit(); } } if (params.containsKey(Probe.PROBE_DISTANCE)) { Object distance = params.get(Probe.PROBE_DISTANCE); if (distance instanceof Double) { distance = ((Double) distance).longValue(); } if (distance instanceof Long) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putString(FusedLocationProbe.DISTANCE, distance.toString()); e.commit(); } } if (params.containsKey(Probe.PROBE_CALIBRATION_NOTIFICATIONS)) { Object enable = params.get(Probe.PROBE_CALIBRATION_NOTIFICATIONS); if (enable instanceof Boolean) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean(FusedLocationProbe.ENABLE_CALIBRATION_NOTIFICATIONS, ((Boolean) enable)); e.commit(); } } } @Override public String summary(Context context) { return context.getString(R.string.summary_fused_location_probe_desc); } @Override @SuppressWarnings("deprecation") public PreferenceScreen preferenceScreen(final Context context, PreferenceManager manager) { PreferenceScreen screen = manager.createPreferenceScreen(context); screen.setTitle(this.title(context)); screen.setSummary(R.string.summary_fused_location_probe_desc); CheckBoxPreference enabled = new CheckBoxPreference(context); enabled.setTitle(R.string.title_enable_probe); enabled.setKey(FusedLocationProbe.ENABLED); enabled.setDefaultValue(FusedLocationProbe.DEFAULT_ENABLED); screen.addPreference(enabled); FlexibleListPreference duration = new FlexibleListPreference(context); duration.setKey(FusedLocationProbe.FREQUENCY); duration.setDefaultValue(Probe.DEFAULT_FREQUENCY); duration.setEntryValues(R.array.probe_satellite_frequency_values); duration.setEntries(R.array.probe_satellite_frequency_labels); duration.setTitle(R.string.probe_frequency_label); screen.addPreference(duration); FlexibleListPreference distance = new FlexibleListPreference(context); distance.setKey(FusedLocationProbe.DISTANCE); distance.setDefaultValue(FusedLocationProbe.DEFAULT_DISTANCE); distance.setEntryValues(R.array.probe_fused_location_distance); distance.setEntries(R.array.probe_fused_location_distance_labels); distance.setTitle(R.string.probe_fused_location_distance_label); screen.addPreference(distance); Preference calibrate = new Preference(context); calibrate.setTitle(R.string.config_probe_calibrate_title); calibrate.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference pref) { Intent intent = new Intent(context, LocationLabelActivity.class); context.startActivity(intent); return true; } }); screen.addPreference(calibrate); CheckBoxPreference enableCalibrationNotifications = new CheckBoxPreference(context); enableCalibrationNotifications.setTitle(R.string.title_enable_calibration_notifications); enableCalibrationNotifications.setSummary(R.string.summary_enable_calibration_notifications); enableCalibrationNotifications.setKey(FusedLocationProbe.ENABLE_CALIBRATION_NOTIFICATIONS); enableCalibrationNotifications.setDefaultValue(FusedLocationProbe.DEFAULT_ENABLE_CALIBRATION_NOTIFICATIONS); screen.addPreference(enableCalibrationNotifications); return screen; } @Override public JSONObject fetchSettings(Context context) { JSONObject settings = super.fetchSettings(context); try { JSONObject enabled = new JSONObject(); enabled.put(Probe.PROBE_TYPE, Probe.PROBE_TYPE_BOOLEAN); JSONArray values = new JSONArray(); values.put(true); values.put(false); enabled.put(Probe.PROBE_VALUES, values); settings.put(Probe.PROBE_CALIBRATION_NOTIFICATIONS, enabled); JSONObject frequency = new JSONObject(); frequency.put(Probe.PROBE_TYPE, Probe.PROBE_TYPE_LONG); values = new JSONArray(); String[] options = context.getResources().getStringArray(R.array.probe_satellite_frequency_values); for (String option : options) { values.put(Long.parseLong(option)); } frequency.put(Probe.PROBE_VALUES, values); settings.put(Probe.PROBE_FREQUENCY, frequency); JSONObject distance = new JSONObject(); distance.put(Probe.PROBE_TYPE, Probe.PROBE_TYPE_LONG); values = new JSONArray(); options = context.getResources().getStringArray(R.array.probe_fused_location_distance); for (String option : options) { values.put(Long.parseLong(option)); } frequency.put(Probe.PROBE_VALUES, values); settings.put(Probe.PROBE_DISTANCE, distance); } catch (JSONException e) { LogManager.getInstance(context).logException(e); } return settings; } @Override public boolean isEnabled(final Context context) { SharedPreferences prefs = Probe.getPreferences(context); if (super.isEnabled(context) && prefs.getBoolean(FusedLocationProbe.ENABLED, FusedLocationProbe.DEFAULT_ENABLED)) { if (ContextCompat.checkSelfPermission(context, "android.permission.ACCESS_FINE_LOCATION") == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(context, "android.permission.ACCESS_COARSE_LOCATION") == PackageManager.PERMISSION_GRANTED) { long now = System.currentTimeMillis(); if (this._probeEnabled == 0) this._probeEnabled = now; this._context = context.getApplicationContext(); long freq = Long.parseLong(prefs.getString(FusedLocationProbe.FREQUENCY, Probe.DEFAULT_FREQUENCY)); long distance = Long.parseLong(prefs.getString(FusedLocationProbe.DISTANCE, FusedLocationProbe.DEFAULT_DISTANCE)); if (this._lastFrequency != freq || this._listening == false || this._lastDistance != distance) { LocationCalibrationHelper.check(context); this._lastFrequency = freq; this._listening = false; if (this._apiClient != null) { if (this._apiClient.isConnected()) { try { LocationServices.FusedLocationApi.removeLocationUpdates(this._apiClient, this); this._apiClient.disconnect(); } catch (IllegalStateException e) { LogManager.getInstance(context).logException(e); } catch (NullPointerException e) { LogManager.getInstance(context).logException(e); } } this._apiClient = null; } this._listening = true; } if (this._apiClient == null) { GoogleApiClient.Builder builder = new GoogleApiClient.Builder(this._context); builder.addConnectionCallbacks(this); builder.addOnConnectionFailedListener(this); builder.addApi(LocationServices.API); this._apiClient = builder.build(); this._apiClient.connect(); } String name = context.getString(R.string.name_location_services_check); String message = context.getString(R.string.message_location_services_check); if (now - this._probeEnabled > FusedLocationProbe.LOCATION_TIMEOUT && now - this._lastReading > FusedLocationProbe.LOCATION_TIMEOUT) { SanityManager.getInstance(context).addAlert(SanityCheck.WARNING, name, message, new Runnable() { @Override public void run() { Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } }); } else SanityManager.getInstance(context).clearAlert(name); } else { SanityManager.getInstance(context).addPermissionAlert(this.name(context), "android.permission.ACCESS_FINE_LOCATION", context.getString(R.string.rationale_fused_location_probe), null); } return true; } else { this._probeEnabled = 0; if (this._apiClient != null) { this._apiClient.disconnect(); this._apiClient = null; } } this._listening = false; return false; } @Override public String title(Context context) { return context.getString(R.string.title_fused_location_probe); } @Override public String name(Context context) { return FusedLocationProbe.NAME; } @Override public void onLocationChanged(Location location) { if (location == null) return; long now = System.currentTimeMillis(); this._lastReading = now; final Bundle bundle = new Bundle(); bundle.putString("PROBE", this.name(this._context)); bundle.putLong("TIMESTAMP", now / 1000); bundle.putDouble(FusedLocationProbe.LATITUDE, location.getLatitude()); bundle.putDouble(FusedLocationProbe.LONGITUDE, location.getLongitude()); bundle.putString(FusedLocationProbe.PROVIDER, location.getProvider()); if (location.hasAccuracy()) bundle.putFloat(FusedLocationProbe.ACCURACY, location.getAccuracy()); if (location.hasAltitude()) bundle.putDouble(FusedLocationProbe.ALTITUDE, location.getAltitude()); if (location.hasBearing()) bundle.putFloat(FusedLocationProbe.BEARING, location.getBearing()); if (location.hasSpeed()) bundle.putFloat(FusedLocationProbe.SPEED, location.getSpeed()); bundle.putLong(FusedLocationProbe.TIME_FIX, location.getTime()); synchronized (this) { long time = location.getTime(); if (time - this._lastCache > 30000 || this._lastLocation == null) { boolean include = true; if (this._lastLocation != null && this._lastLocation.distanceTo(location) < 50.0) include = false; if (include) { Map<String, Object> values = new HashMap<>(); values.put(LocationProbe.LONGITUDE_KEY, location.getLongitude()); values.put(LocationProbe.LATITUDE_KEY, location.getLatitude()); values.put(ProbeValuesProvider.TIMESTAMP, (double) (location.getTime() / 1000)); ProbeValuesProvider.getProvider(this._context).insertValue(this._context, FusedLocationProbe.DB_TABLE, FusedLocationProbe.databaseSchema(), values); this._lastCache = time; this._lastLocation = new Location(location); } } final FusedLocationProbe me = this; Runnable r = new Runnable() { @Override public void run() { FoursquareProbe.annotate(me._context, bundle); me.transmitData(me._context, bundle); } }; Thread t = new Thread(r); t.start(); } } @Override public String summarizeValue(Context context, Bundle bundle) { double latitude = bundle.getDouble(FusedLocationProbe.LATITUDE); double longitude = bundle.getDouble(FusedLocationProbe.LONGITUDE); return String.format(context.getResources().getString(R.string.summary_location_probe), latitude, longitude); } @Override public void onConnected(Bundle bundle) { final LocationRequest request = new LocationRequest(); request.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY); SharedPreferences prefs = Probe.getPreferences(this._context); long freq = Long.parseLong(prefs.getString(FusedLocationProbe.FREQUENCY, Probe.DEFAULT_FREQUENCY)); request.setInterval(freq); long distance = Long.parseLong(prefs.getString(FusedLocationProbe.DISTANCE, FusedLocationProbe.DEFAULT_DISTANCE)); if (distance != 0) request.setSmallestDisplacement(distance); try { if (this._apiClient != null && this._apiClient.isConnected()) LocationServices.FusedLocationApi.requestLocationUpdates(this._apiClient, request, this, this._context.getMainLooper()); } catch (IllegalStateException e) { LogManager.getInstance(this._context).logException(e); } } @Override public void onConnectionSuspended(int i) { if (this._apiClient != null && this._apiClient.isConnected()) LocationServices.FusedLocationApi.removeLocationUpdates(this._apiClient, this); } @Override public void onConnectionFailed(ConnectionResult connectionResult) { this._apiClient = null; } public static Map<String, String> databaseSchema() { HashMap<String, String> schema = new HashMap<>(); schema.put(LocationProbe.LATITUDE_KEY, ProbeValuesProvider.REAL_TYPE); schema.put(LocationProbe.LONGITUDE_KEY, ProbeValuesProvider.REAL_TYPE); return schema; } @Override public Intent viewIntent(Context context) { try { Class.forName("com.google.android.maps.MapActivity"); Intent i = new Intent(context, LocationProbeActivity.class); i.putExtra(LocationProbeActivity.DB_TABLE_NAME, FusedLocationProbe.DB_TABLE); return i; } catch (Exception e) { return super.viewIntent(context); } } }